package polygon.util;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import polygon.util.impl.UnsafeFileUtil;

import java.io.*;
import java.util.List;

/**
 * @author Mike Mirzayanov
 */                                    
public class FileUtil {
    private static final Logger logger = Logger.getLogger(FileUtil.class);

    private static <T> T execute(Operation<T> operation) throws IOException {
        IOException ioException = null;
        RuntimeException runtimeException = null;

        for (int i = 0; i < 5; i++) {
            try {
                return operation.run();
            } catch (IOException e) {
                ioException = e;
                try {
                    if (i > 0) {
                        logger.warn("IOException found after the " + i + "th iteration: " + e.getMessage(), e);
                    }
                    Thread.sleep(i * 250);
                } catch (InterruptedException e1) {
                    // No operation.
                }
            } catch (RuntimeException e) {
                runtimeException = e;
                try {
                    logger.warn("RuntimeException found after the " + i + "th iteration: " + e.getMessage(), e);
                    Thread.sleep(i * 250 + 50);
                } catch (InterruptedException e1) {
                    // No operation.
                }
            }
        }

        if (ioException != null) {
            throw ioException;
        }

        if (runtimeException != null) {
            throw runtimeException;
        }

        throw new RuntimeException("This line shouldn't be executed.");
    }

    /**
     * Copies one file to another. Overwrites it if exists.
     *
     * @param source      Source file.
     * @param destination Destination file.
     * @throws IOException Can't perform copy.
     */
    public static void copyFile(final File source, final File destination) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.copyFile(source, destination);
                return null;
            }
        });
    }

    /**
     * Copy one directory into another. If the second one exists it copies nested files from
     * the source to destination.
     *
     * @param source      Source directory.
     * @param destination Destination directory.
     * @throws IOException when can't perform copy.
     */
    public static void copyDirectory(final File source, final File destination) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.copyDirectory(source, destination);
                return null;
            }
        });
    }

    /**
     * Deletes file or directory. Finishes quitely in case of no such file.
     * Directory will be deleted with each nested element.
     *
     * @param file File to be deleted.
     * @throws IOException if can't delete file.
     */
    public static void deleteTotaly(final File file) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.deleteTotaly(file);
                return null;
            }
        });
    }


    /**
     * Deletes file or directory. Finishes quitely in _any_ case.
     * Will start new thread.
     *
     * @param file File to be deleted.
     */
    public static void deleteTotalyAsync(final File file) {
        new Thread() {
            public void run() {
                try {
                    deleteTotaly(file);
                } catch (Throwable e) {
                    // No operations.
                }
            }
        }.start();
    }

    /**
     * @param reader Reader to be processed.
     * @return String containing all characters from reader.
     * @throws IOException if can't read data.
     */
    public static String readFromReader(Reader reader) throws IOException {
        return UnsafeFileUtil.readFromReader(reader);
    }

    /**
     * @param file File to be read.
     * @return String containing file data.
     * @throws IOException if can't read file. Possibly, file parameter
     *                     doesn't exists, is directory or not enought permissions.
     */
    public static String readFile(final File file) throws IOException {
        return execute(new Operation<String>() {
            public String run() throws IOException {
                return UnsafeFileUtil.readFile(file);
            }
        });
    }

    /**
     * Writes new file into filesystem. Overwrite existing if exists.
     * Creates parent directory if needed.
     *
     * @param file        File to be write.
     * @param inputStream Input stream to get data.
     * @throws java.io.IOException if can't read file.
     */
    public static void writeFile(File file, InputStream inputStream) throws IOException {
        UnsafeFileUtil.writeFile(file, inputStream);
    }

    /**
     * Writes new file into filesystem. Overwrite existing if exists.
     * Creates parent directory if needed.
     *
     * @param file    File to be write.
     * @param content Content to be write.
     * @throws java.io.IOException if can't read file.
     */
    public static void writeFile(final File file, final String content) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.writeFile(file, content);
                return null;
            }
        });
    }


    /**
     * Writes new file into filesystem. Overwrite existing if exists.
     * Creates parent directory if needed.
     *
     * @param file     File to be write.
     * @param content  Content to be write.
     * @param encoding File encoding.
     * @throws IOException                  if can't read file.
     * @throws UnsupportedEncodingException Illegal encoding.
     */
    public static void writeFile(final File file, final String content, final String encoding) throws IOException, UnsupportedEncodingException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.writeFile(file, content, encoding);
                return null;
            }
        });
    }

    /**
     * Writes new file into filesystem. Overwrite existing if exists.
     * Creates parent directory if needed.
     *
     * @param file  File to be write.
     * @param bytes Bytes to be write.
     * @throws java.io.IOException if can't write file.
     */
    public static void writeFile(final File file, final byte[] bytes) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.writeFile(file, bytes);
                return null;
            }
        });
    }

    /**
     * Very like to writeFile but doesn't overwrite file.
     * Creates parent directory if needed.
     *
     * @param file  File to write.
     * @param bytes Bytes to write into file.
     * @throws IOException If file exists or can't write file.
     */
    public static void createFile(final File file, final byte[] bytes) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.createFile(file, bytes);
                return null;
            }
        });
    }

    /**
     * Very like to writeFile but doesn't overwrite file.
     *
     * @param file    File to write.
     * @param content String to write into file.
     * @throws IOException If file exists or can't write file.
     */
    public static void createFile(final File file, final String content) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.createFile(file, content);
                return null;
            }
        });
    }

    /**
     * Parses XML string and extracts value.
     *
     * @param xml   XML to be scanned.
     * @param xpath Xpath expression.
     * @param clazz String.class or Integer.class are supported now.
     * @param <T>   Return type.
     * @return Return value.
     * @throws IOException In case of I/O error.
     */
    public static synchronized <T> T extractFromXml(final File xml, final String xpath, final Class<T> clazz) throws IOException {
        return execute(new Operation<T>() {
            public T run() throws IOException {
                return UnsafeFileUtil.extractFromXml(xml, xpath, clazz);
            }
        });
    }

    /**
     * Writes XML document into file.
     *
     * @param file     File to write.
     * @param document XML document.
     * @throws IOException In case of I/O error.
     */
    public static void writeXml(final File file, final Document document) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.writeXml(file, document);
                return null;
            }
        });
    }

    /**
     * Changes the value describing by xpath to specific value. And updates file.
     *
     * @param file  Which will read first and updated later.
     * @param xpath Xpath to find specific Node.
     * @param value Value to be set for found node.
     * @throws IOException In case of I/O error.
     */
    public static synchronized void updateXml(final File file, final String xpath, final String value) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.updateXml(file, xpath, value);
                return null;
            }
        });
    }

    /**
     * @param file File to remove.
     * @throws IOException If file not found or can't be removed.
     */
    public static void removeFile(final File file) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.removeFile(file);
                return null;
            }
        });
    }

    /**
     * Renames source to destination. Or throws IOException if can't.
     *
     * @param sourceFile      Source.
     * @param destinationFile Destination.
     * @throws IOException if can't rename.
     */
    public static void renameFile(final File sourceFile, final File destinationFile) throws IOException {
        execute(new Operation<Void>() {
            public Void run() throws IOException {
                UnsafeFileUtil.renameFile(sourceFile, destinationFile);
                return null;
            }
        });
    }

    /**
     * @param file File to be read.
     * @return File content as a byte array.
     * @throws IOException           if can't read file.
     * @throws FileNotFoundException if can't find file.
     */
    public static byte[] getBytes(final File file) throws IOException {
        return execute(new Operation<byte[]>() {
            public byte[] run() throws IOException {
                return UnsafeFileUtil.getBytes(file);
            }
        });
    }

    /**
     * Returns first 255 bytes of the file. Returns smaller number of bytes it it contains less.
     *
     * @param file File to be read.
     * @return File content as a byte array.
     * @throws IOException           if can't read file.
     * @throws FileNotFoundException if can't find file.
     */
    public static FirstBytes getFirstBytes(final File file) throws IOException {
        return execute(new Operation<FirstBytes>() {
            public FirstBytes run() throws IOException {
                return UnsafeFileUtil.getFirstBytes(file);
            }
        });
    }

    /**
     * Creates temporary directory with autogenerated name with specific prefix.
     *
     * @param prefix Prefix for directory name.
     * @return File instance.
     * @throws IOException if can't create directory.
     */
    public static File createTemporaryDirectory(final String prefix) throws IOException {
        return execute(new Operation<File>() {
            public File run() throws IOException {
                return UnsafeFileUtil.createTemporaryDirectory(prefix);
            }
        });
    }

    /**
     * @param file Any file.
     * @return String Name part (simple name without extension).
     */
    public static String getName(File file) {
        return UnsafeFileUtil.getName(file);
    }

    /**
     * @param file Any file.
     * @return String Extension with dot in lowercase. For example, ".cpp".
     */
    public static String getExt(File file) {
        return UnsafeFileUtil.getExt(file);
    }

    /**
     * @return System temporary directory. It expected that current process
     *         has permissions for read, write and execution in it.
     * @throws IOException error.
     */
    public static File getTemporaryDirectory() throws IOException {
        return execute(new Operation<File>() {
            public File run() throws IOException {
                return UnsafeFileUtil.getTemporaryDirectory();
            }
        });
    }

    /**
     * @param directory Directory to be scanned.
     * @return List of nested files (scans nested directories recursivly). Doesn't scan
     *         hidden directories and doesn't return hidden files.
     * @throws IOException error.
     */
    public static List<File> list(final File directory) throws IOException {
        return execute(new Operation<List<File>>() {
            public List<File> run() throws IOException {
                return UnsafeFileUtil.list(directory);
            }
        });
    }

    public static long getDirectorySize(final File directory) throws IOException {
        return execute(new Operation<Long>() {
            public Long run() throws IOException {
                return UnsafeFileUtil.getDirectorySize(directory);
            }
        });
    }

    public static class FirstBytes {
        private final boolean truncated;
        private final byte[] bytes;

        public FirstBytes(boolean truncated, byte[] bytes) {
            this.truncated = truncated;
            this.bytes = bytes;
        }

        public boolean isTruncated() {
            return truncated;
        }

        public byte[] getBytes() {
            return bytes;
        }
    }

    /**
     * Accepts not fidden files.
     */
    public static class NotHiddenFileFilter implements FilenameFilter {
        public boolean accept(File dir, String name) {
            return !new File(dir, name).isHidden();
        }
    }

    private interface Operation<T> {
        T run() throws IOException;
    }
}
